Skip to content

feat(ui): shareable deep links, rendered detail views, component-detail 502 fix - #665

Merged
padak merged 3 commits into
mainfrom
feat/ui-deep-links-rendered-details
Aug 23, 2026
Merged

feat(ui): shareable deep links, rendered detail views, component-detail 502 fix#665
padak merged 3 commits into
mainfrom
feat/ui-deep-links-rendered-details

Conversation

@padak

@padak padak commented Aug 23, 2026

Copy link
Copy Markdown
Member

UI/UX audit follow-up for the NERD web UI (kbagent serve --ui), driven by a live click-through against real e2e projects. Three complaints, three fixes — plus one server-side bug the audit surfaced.

1. Shareable deep links (hash router)

The URL never changed while navigating, so a state seen in the UI could not be sent to a colleague. Now:

#/p/e2e-snowflake/configs?sel=keboola.ex-db-snowflake%2F01ky4pga...   config detail drawer
#/p/e2e-snowflake/storage?sel=tables%2Fin.c-foo.bar                   storage tab + table drawer
#/doctor                                                              page without project context
  • Hash-based by design: the REST API owns root paths (GET /projects is JSON), so history-mode routing would collide with it; the hash needs zero server changes and survives the static SPA mount.
  • New src/router.ts — pure parseHash/buildHash, no dependencies, 38 vitest tests (round-trips, encoded/garbage input, unknown-page fallback). First test file in web/frontendfrontend.yml now runs npm test (the step its own comment reserved for this moment).
  • Page changes push history (browser Back walks pages); selection/branch changes use replaceState (a row click never costs an extra Back press). hashchange is applied back to state, so Back/Forward and hand-edited URLs work.
  • A link with an alias this install doesn't know falls back to the default project and drops the stale branch/sel instead of leaving every page erroring.
  • Adopted by Projects, Configs, Components, Data Apps, Jobs, Storage (tab + object id), Streams, Flows — each with a one-time restore after the first list load, so a cold reload of a shared link re-opens the drawer.
  • @tanstack/react-router removed from package.json: declared, never imported anywhere.

2. Rendered detail views (Overview / Raw JSON everywhere)

Detail views that answered a click with a bare JSON.stringify dump now render a structured Overview, with the untouched payload one tab away (nothing the API returned is hidden). Pattern and styling follow the existing Jobs drawer / Streams panel — no new design language.

Page Before After
Projects raw dump in an inline card below the table wide drawer: project + token cards, feature pills, limits table, open in Keboola UI →
Configs raw dump inside the #658 drawer metadata grid, per-key parameter pills over the configuration body, rows table, collapsed state
Components raw dump metadata, markdown long_description, schema summary, catalog-fallback note
Data Apps inline dump of the list row, window.confirm delete wide drawer fetching the real detail route; Overview / Logs / Raw tabs (logs = tail 200, tab-activated); ConfirmModal delete

Shared primitives added once and reused: DetailTabs, KeyValueGrid, PillList, RawDetail (Overview/Raw tabs + copy-JSON with a non-secure-origin fallback). Drawer gains an optional wide prop and an expand toggle that stops at the sidebar — existing consumers are byte-identical in behavior.

3. Component detail 502 (server)

GET /components/keboola.mcp-server-tool?project=… answered 502 Bad Gateway for components the project can demonstrably run (they're in the list). Root cause: component detail reads the AI Service, which indexes the public catalog only; private/deprecated components 404 there, and the serve error handler mapped every KeboolaApiError to 502.

  • ComponentService.get_component_detail: an AI Service NOT_FOUND now falls back to the project's Storage component catalog — same response shape, new documentation_source: "ai_service" | "storage_catalog" discriminator on both paths. NOT_FOUND still raised when both sources miss; non-404 AI failures are never masked. This fixes the CLI command too.
  • serve: ErrorCode.NOT_FOUND now maps to HTTP 404 (session codes stay 401, the rest stays 502) — an upstream "does not exist" is not a gateway fault.
  • The UI notes the fallback: "AI Service has no documentation for this component — showing the project's Storage catalog entry."

Also

  • react-query runs with networkMode: "always": the SPA only ever talks to the localhost origin that served it, so the online/offline heuristic was never right — a browser that (correctly or spuriously) reports offline silently paused queries and froze the UI with no spinner and no error while kbagent serve kept working.
  • Docs: docs/web-server.md (deep links + 404 mapping), gotchas.md + commands-reference.md + CLAUDE.md for the component-detail fallback, all tagged (since vNEXT) per the docs: version bumps move out of feature PRs into dedicated release PRs #648 release process. No version bump, no changelog entry (release PR's job).

Verification

  • make check — 6017 passed, 12 skipped (includes new tests: service fallback ×5, route 404/502 mapping, smoke handler).
  • web/frontend: tsc --noEmit clean, vite build clean, vitest 38/38.
  • Live walkthrough on kbagent serve --ui against real e2e projects (light + dark): cold-reload restore of deep links on Projects/Configs/Components/Data Apps/Jobs/Storage/Streams, Back/Forward behavior, catalog-fallback note on keboola.mcp-server-tool (200 instead of 502), Logs tab error surfacing, copy-JSON fallback.

Open in Devin Review

padak added 2 commits August 23, 2026 19:55
…component; map NOT_FOUND to HTTP 404 over serve

The AI Service indexes the public component catalog only, so a private or
deprecated component the project can run (keboola.mcp-server-tool,
keboola.data-apps) 404'd in 'component detail' while 'component list'
showed it -- and kbagent serve surfaced that as HTTP 502 Bad Gateway.

- ComponentService.get_component_detail: an AI Service NOT_FOUND now falls
  back to the project's Storage component catalog, same response shape,
  discriminated by a new documentation_source key present on both paths.
  NOT_FOUND is still raised when both sources miss; non-404 AI failures
  are never masked.
- serve: KeboolaApiError with ErrorCode.NOT_FOUND maps to HTTP 404
  (session codes stay 401, everything else stays 502).
- Human-mode component detail prints a source note for the fallback.
- Docs: gotchas.md + commands-reference.md + CLAUDE.md + web-server.md,
  tagged (since vNEXT) per the release process.
…D web UI

Every place the SPA answered a click with a raw JSON.stringify dump now
renders a structured Overview with the untouched payload one tab away,
and the URL finally encodes where you are so a link can be shared.

Deep links (hash-based -- the REST API owns root paths, so history-mode
routing would collide):
  #/<page>                      page without project context
  #/p/<project>/<page>          project-scoped page
  ?branch=<id>                  non-default branch
  ?sel=<encoded>                page-owned selected object
- New src/router.ts (pure parse/build, 38 vitest tests) + useHashSelection
  hook; state.tsx seeds from the hash, syncs it back (pushState for page
  changes so Back works, replaceState for selection), applies hashchange.
- A URL alias unknown to this install falls back to the default project
  and drops the stale branch/sel instead of erroring every page.
- Adopted by Projects, Configs, Components, Data Apps, Jobs, Storage
  (tab + object), Streams, Flows.
- @tanstack/react-router removed from package.json -- declared, never
  imported.

Rendered detail views (pattern set by the Jobs drawer):
- New shared primitives: DetailTabs, KeyValueGrid, PillList, RawDetail
  (Overview / Raw JSON tabs + copy-JSON with non-secure-origin fallback).
- Drawer: optional wide prop + expand toggle stopping at the sidebar.
- Projects: raw dump -> wide drawer with project/token cards, feature
  pills, limits table, open-in-Keboola link.
- Configs: raw dump -> metadata grid, per-key parameter pills over the
  configuration body, rows table, collapsed state.
- Components: raw dump -> metadata, markdown long_description, schema
  summary, catalog-fallback note driven by documentation_source.
- Data Apps: inline list-row dump -> wide drawer fetching the real detail
  route, Overview / Logs (tail 200, tab-activated) / Raw tabs; delete
  confirm moved from window.confirm to ConfirmModal.

react-query now runs with networkMode 'always': the SPA only ever talks
to the localhost origin that served it, so the online/offline heuristic
could silently pause queries and freeze the UI while serve kept working.

CI: frontend.yml gains the npm test step its own comment reserved for
the first real test file.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +30 to +47
//
// A project pinned by the URL (`#/p/<alias>/...`) WINS: the UI state is
// seeded from the hash before the first render, so `project` is already set
// by the time this effect runs and the early return leaves it alone. The one
// exception is an alias this installation does not know -- a link shared from
// someone else's machine, or a project since renamed/removed. Every page
// keys its queries off the alias, so keeping it would render nothing but
// errors; fall back to the default and drop the branch/selection that were
// scoped to it.
useEffect(() => {
if (project) return;
const projects = projectsQ.data?.projects;
if (!projects?.length) return;
if (project && projects.some((p) => p.alias === project)) return;
const def = projects.find((p) => p.is_default) ?? projects[0];
// `setProject` clears the selection; the branch id is project-scoped too.
setProject(def.alias);
}, [project, projectsQ.data, setProject]);
if (project) setBranchId(null);
}, [project, projectsQ.data, setProject, setBranchId]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Deep-linked drawer can briefly open for an unknown project

For a shared link like #/p/<unknown>/jobs?sel=123 with an unregistered alias, TopBar falls back to the default project and clears sel, but each page's one-time restore effect can run first and open a drawer scoped to the unknown alias. The page's local selected is not cleared by the fallback, so a drawer with an errored detail fetch can linger. Timing-dependent (list-query settle order), so not flagged as a definite bug; documented intent is to open with no drawer.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 44cc249. The Jobs page was the only adopter whose restore path opened a drawer without a list hit (the deliberate synthetic-row fallback for jobs outside the 100-row cap). That fallback is now gated on the list query having actually loaded — an errored list (the unknown-alias case, right before TopBar swaps to the default project) drops the deep link instead of pinning an errored drawer to a project that is being swapped away. Verified live: #/p/<unknown>/jobs?sel=... now lands on the default project's Jobs page with no drawer; a valid-project link to an old job still opens the synthetic drawer as designed.

…st errored

Devin review: a shared link with a project alias this install does not
know (#/p/<unknown>/jobs?sel=...) could open the synthetic job drawer
against the unknown alias before TopBar's fallback swapped to the
default project, leaving a drawer with an errored detail fetch behind.
The synthetic fallback is now gated on the list having actually loaded
(valid project, job merely outside the 100-row cap); an errored list
drops the deep link instead. Verified live: the foreign link now lands
on the default project's Jobs page with no drawer.
@padak
padak merged commit 3ea9c87 into main Aug 23, 2026
5 checks passed
@padak
padak deleted the feat/ui-deep-links-rendered-details branch August 23, 2026 18:18
padak added a commit that referenced this pull request Aug 23, 2026
The deep-link restore effect from #665 was mount-oriented: a one-shot
ref meant a `?sel=` written by the command palette while Storage was
already open (or against a stale, still-filtered tables list) was
consumed without opening anything, stranding the drawer shut.

Split it into two effects: one adopts an externally-written sel (tab,
bucket filter -- dropping a filter that cannot contain the target
table), one opens the table once a list containing it has loaded, with
no give-up flag (a closed drawer rewrites sel, so it cannot re-open).

Verified live: palette table jump with a matching filter (kept), with a
conflicting filter (dropped), and cold reload of both URL forms.
padak added a commit that referenced this pull request Aug 23, 2026
The command palette now finds storage objects: the active project's buckets and tables plus every registered project's buckets, fetched on palette open (react-query cache shared with the Storage page; cross-project bucket fan-out at 60s staleTime) and fuzzy-matched locally, so typing never waits on the network. Cross-project tables are deliberately not loaded (reachable via their bucket).

Picking an object navigates by writing the Storage page's ?sel= selection via its own grammar helper, so a palette jump produces the same shareable URL as clicking the row. The sel grammar gains a bucket/<bucketId> form (the filter chip now survives reload); a non-empty query always offers a final 'Search across projects' row that jumps to the Search page and auto-runs via a pendingSearchQuery hand-off slot. Also fixes the mount-only assumption in the #665 restore effect so a sel written into an already-mounted Storage page (palette retarget, incompatible bucket filter) applies. New storageSel.test.ts vitest suite (11 tests).
@padak padak mentioned this pull request Aug 23, 2026
10 tasks
padak added a commit that referenced this pull request Aug 23, 2026
* chore(release): 0.90.0

Bumps pyproject.toml to 0.90.0 and adds the changelog entry covering every
PR merged since v0.89.0 (#658, #662, #661, #663, #665, #666, #664, #668,
#667, #623), resolves the vNEXT placeholders those PRs left behind, and
adds the curated What's new reel for the release.

* docs(web-server): keep the What's-new anchor stable across releases

The '### What's-new popup *(since vNEXT)*' heading put the version gate in
the heading itself, so resolving the placeholder to 0.90.0 changed the
generated slug to 'whats-new-popup-since-0900' and broke the in-page link
at line 138 -- and would have broken it again on every future release.

Moved the '(since 0.90.0)' tag to the first body line: the anchor is now
the stable 'whats-new-popup', the gate stays visible, and
check_version_gates.py still sees it (it scans the whole file, not just
headings).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant